#include <arpa/inet.h>
#include <pthread.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <unistd.h>

unsigned int local_udp_port = 18000;
unsigned int dest_udp_port = 18000;
// char dest_ip[16] = "47.94.142.231";
char dest_ip[16] = "127.0.0.1";
unsigned int local_tcp_port = 18001;
unsigned int dest_tcp_port = 18001;

// 标识位，用于标志程序是否被终止
bool exited = false;
bool send_file_err = false;
bool recv_file_err = false;
bool ready_send_file = false;
bool ready_recv_file = false;

int udp_sock = -1;
int tcp_sock = -1;

struct sockaddr_in local_tcp_addr = {0};
struct sockaddr_in local_udp_addr = {0};
struct sockaddr_in dest_tcp_addr = {0};
struct sockaddr_in dest_udp_addr = {0};

// 定义udp和tcp的发送和接收缓存
char udp_send_buf[1024] = {0};
char udp_recv_buf[1024] = {0};
char tcp_send_buf[1024] = {0};
char tcp_recv_buf[1024] = {0};

char file_path[256] = {0};

// 消息接收线程句柄
pthread_t msg_recv_tid;
// 文件发送线程句柄
pthread_t file_send_tid;
// 文件接收线程句柄
pthread_t file_recv_tid;
// 状态消息发送线程
pthread_t status_send_tid;

// 初始化地址信息
void init_addr() {
  // 本机udp地址信息
  local_udp_addr.sin_family = AF_INET;
  local_udp_addr.sin_port = htons(local_udp_port);
  local_udp_addr.sin_addr.s_addr = INADDR_ANY;

  // 目标udp地址信息
  dest_udp_addr.sin_family = AF_INET;
  dest_udp_addr.sin_port = htons(dest_udp_port);
  dest_udp_addr.sin_addr.s_addr = inet_addr(dest_ip);

  // 本机tcp地址信息
  local_tcp_addr.sin_family = AF_INET;
  local_tcp_addr.sin_port = htons(local_tcp_port);
  local_tcp_addr.sin_addr.s_addr = INADDR_ANY;

  // 目标tcp的地址信息
  dest_tcp_addr.sin_family = AF_INET;
  dest_tcp_addr.sin_port = htons(dest_tcp_port);
  inet_aton(dest_ip, &dest_tcp_addr.sin_addr);
}

void init_udp_sock() {
  // 创建udp套接字
  udp_sock = socket(AF_INET, SOCK_DGRAM, 0);

  if (udp_sock < 0) {
    perror("create udpsocket failed.\n");
    exit(-1);
  }

  int ret = bind(udp_sock, (struct sockaddr *)&local_udp_addr,
                 sizeof(local_udp_addr));

  if (ret < 0) {
    perror("bind udpsock failed.");
    close(udp_sock);
    exit(-1);
  }
}

void *recv_file(void *arg) {
  char buf[1024];

  tcp_sock = socket(AF_INET, SOCK_STREAM, 0);

  if (tcp_sock < 0) {
    perror("tcp socket create err.\n");
    recv_file_err = true;
    close(tcp_sock);
    return NULL;
  }

  int ret = bind(tcp_sock, (struct sockaddr *)&local_tcp_addr,
                 sizeof(local_tcp_addr));

  if (ret < 0) {
    perror("tcp socket bind err.\n");
    recv_file_err = true;
    close(tcp_sock);
    return NULL;
  }

  // 最多监听一个
  ret = listen(tcp_sock, 1);

  if (ret < 0) {
    perror("tcp socket listen err.\n");
    recv_file_err = true;
    close(tcp_sock);
    return NULL;
  }

  struct sockaddr_in client_addr = {0};
  socklen_t socklen;

  puts("tcp server begin listen!");
  ready_recv_file = true;

  while (!exited) {
    int sock = accept(tcp_sock, (struct sockaddr *)&client_addr,
                      (socklen_t *)&socklen);
    puts("tcp accepted a client");

    bzero(buf, sizeof(buf));

    // 接收文件名
    int recv_len = recv(sock, buf, sizeof(buf), 0);

    if (recv_len < 0) {
      perror("recv data error!\n");
      break;
    }

    char filename[64];
    strncpy(filename, buf, sizeof(filename));

    printf("Ready to recv file %s\n", filename);

    FILE *fp = fopen(filename, "w");

    if (fp == NULL) {
      fprintf(stderr, "receiver open file error: %s\n", buf);
      fclose(fp);
      close(sock);
      break;
    }

    printf("receiving file: %s\n", filename);

    bzero(buf, sizeof(buf));

    int len = -1;

    while ((len = recv(sock, buf, sizeof(buf), 0))) {
      if (len < 0) {
        fprintf(stderr, "receive file error: %s\n", filename);
        break;
      }

      int write_length = fwrite(buf, sizeof(char), len, fp);

      if (write_length < len) {
        printf("write file error: %s\n", filename);
        break;
      }

      bzero(buf, sizeof(buf));
    }

    printf("file receive finish: %s\n", filename);

    close(sock);
    fclose(fp);

    break;
  }

  close(tcp_sock);

  return NULL;
}

void *recv_msg(void *arg) {
  struct sockaddr_in src_addr = {0}; // 发送方地址
  socklen_t src_addr_len = sizeof(src_addr);

  while (!exited) {
    bzero(udp_recv_buf, sizeof(udp_recv_buf));
    int ret = recvfrom(udp_sock, udp_recv_buf, sizeof(udp_recv_buf), 0,
                       (struct sockaddr *)&src_addr, &src_addr_len);

    if (ret < 0) {
      puts("\nrecv msg error!");
      continue;
    }

    if (strcmp(udp_recv_buf, "\\exit") == 0) {
      exited = true;
    } else if (strcmp(udp_recv_buf, "\\file") == 0) {
      pthread_create(&file_recv_tid, NULL, recv_file, NULL);
    } else if (strcmp(udp_recv_buf, "\\readyfile") == 0) {
      ready_send_file = true;
    }

    printf("\n[%s:%d]", inet_ntoa(src_addr.sin_addr), ntohs(src_addr.sin_port));
    printf(" >> %s\n\n", udp_recv_buf);
  }

  return NULL;
}

void *send_file(void *arg) {
  int path_len = strlen(file_path);
  char file_name[64] = {0};
  int i = path_len - 1, name_len = 0;

  // 从路径中截取文件名
  while (i >= 0 && file_path[i] != '/')
    --i;

  while (++i < path_len)
    file_name[name_len++] = file_path[i];

  // 打开文件
  FILE *fp = fopen(file_path, "r");

  if (fp == NULL) {
    fprintf(stderr, "sender open file error: %s\n", file_path);
    send_file_err = true;
    return NULL;
  }

  // 忙等待，等待tcp服务端的建立
  while (!ready_send_file && !exited) {
  }
  ready_send_file = false;

  int sock = socket(AF_INET, SOCK_STREAM, 0);

  if (-1 ==
      connect(sock, (struct sockaddr *)&dest_tcp_addr, sizeof(dest_tcp_addr))) {
    send_file_err = true;
    perror("cannot open file connection.\n");
    close(sock);
    fclose(fp);
    return NULL;
  }

  send(sock, file_name, strlen(file_name) + 1, 0);
  printf("tcp send = %s", file_name);

  char file_buf[1024] = {0};
  int file_block_len = 0;

  while ((file_block_len =
              fread(file_buf, sizeof(char), sizeof(file_buf), fp)) > 0) {
    if (send(sock, file_buf, file_block_len, 0) < 0) {
      fprintf(stderr, "send file error: %s\n", file_path);
      send_file_err = true;
      break;
    }

    bzero(file_buf, sizeof(file_buf));
  }

  fclose(fp);
  close(sock);

  return NULL;
}

void prepare_send_file() {
  // 准备发送文件
  bzero(file_path, sizeof(file_path));

  printf("input file path << ");
  fgets(file_path, sizeof(file_path), stdin);

  int len = strlen(file_path);
  file_path[len - 1] = '\0';

  pthread_create(&file_send_tid, NULL, send_file, NULL);
}

void send_msg() {
  int len = 0; // 发送缓存区的长度

  while (!exited) {
    bzero(udp_send_buf, sizeof(udp_send_buf));
    // 从键盘读取要发送的消息
    fgets(udp_send_buf, sizeof(udp_send_buf), stdin);
    len = strlen(udp_send_buf);
    udp_send_buf[--len] = '\0';

    if (strcmp(udp_send_buf, "\\file") == 0) {
      prepare_send_file();
    }

    // \0也需要发出去
    sendto(udp_sock, udp_send_buf, len + 1, 0,
           (struct sockaddr *)&dest_udp_addr, sizeof(dest_udp_addr));

    if (strcmp(udp_send_buf, "\\exit") == 0) {
      pthread_cancel(msg_recv_tid);
      printf("\nprocess exit!\n");
      break;
    }
  }
}

void *send_status() {
  int len = 0;
  char buf[64];

  while (true) {
    if (send_file_err) {
      send_file_err = false;
      strcpy(udp_send_buf, "\\exit");
      len = strlen(udp_send_buf);
    } else if (recv_file_err) {
      recv_file_err = false;
      strcpy(udp_send_buf, "\\exit");
      len = strlen(udp_send_buf);
    } else if (ready_recv_file) {
      ready_recv_file = false;
      strcpy(udp_send_buf, "\\readyfile");
      len = strlen(udp_send_buf);
      printf("send readyfile to client\n");
    } else {
      continue;
    }

    // \0也需要发出去
    sendto(udp_sock, udp_send_buf, len + 1, 0,
           (struct sockaddr *)&dest_udp_addr, sizeof(dest_udp_addr));
  }
}

int main(int argc, char *argv[]) {
  if (argc == 2) {
    strncpy(dest_ip, argv[1], sizeof(dest_ip));
  } else if (argc > 2) {
    fprintf(stderr, "usage: ./im [ip]\n");
    exit(-1);
  }

  init_addr();
  init_udp_sock();

  pthread_create(&msg_recv_tid, NULL, recv_msg, NULL);
  pthread_create(&status_send_tid, NULL, send_status, NULL);

  puts("start chart!");
  send_msg();

  close(tcp_sock);
  close(udp_sock);

  return 0;
}
